import xml.parsers.expat

# TODO validation (e.g. warn on unused arguments)
# TODO simplify filters. <foofilter ...>...</foofilter> is too verbose. Maybe
#      keep on stack if empty, e.g. <foofilter ... />?
# TODO configure number of parallel threads (and similar globals)
# TODO better reporting of invalid REs
# TODO case insensitivity

class ConfigParser:
    writer_names = [ 'writepls', 'writeplsx' ]
    filter_names = [ 'hostfilter', 'pathfilter', 'plsfilter',
                     'titlefilter', 'urlfilter' ]
    suck_args = [ 'depth', 'name', 'leavedomain', 'loglevel' ]

    def __init__(self):
        self._stack = {}
        self._sucks = []
        self._parser = xml.parsers.expat.ParserCreate()
        self._parser.StartElementHandler = self._start_element
        self._parser.EndElementHandler = self._end_element
        self._parser.CharacterDataHandler = self._char_data

    def parse_file(self, file):
        self.__init__()
        file_content = file.read().replace("&", "&amp;")
        self._parser.Parse(file_content)
        return self._sucks

    def _start_element(self, name, attributes):
        n = name.lower()
        a = {}
        for key, value in attributes.items():
            a[key.lower()] = value

        if n == 'suck':
            assert not self._stack.has_key('suck')  # no nesting
            assert a.has_key('seed')
            suck = { 'seed': a['seed'] }
            # Copy supported attributes, ignore others.
            for arg in ConfigParser.suck_args:
                self._copy_attr_if_set(a, arg, suck)
            for filter in ConfigParser.filter_names:
                self._set_filter(filter, suck)
            self._set_writers(suck)
            self._sucks.append(suck)

        if not self._stack.has_key(n):
            self._stack[n] = []
        self._stack[n].append(a)

    def _char_data(self, data):
        pass

    def _end_element(self, name):
        n = name.lower()
        assert self._stack.has_key(n)
        self._stack[n].pop()
        if not self._stack[n]:
            del(self._stack[n])

    def _build_filter(self, name):
        filter = []
        if not self._stack.has_key(name):
            return filter
        for rule in self._stack[name]:
            if rule.has_key('include'):
                filter.append((rule['include'], True))
                assert not rule.has_key('exclude'), filter
            elif rule.has_key('exclude'):
                filter.append((rule['exclude'], False))
                assert not rule.has_key('include'), filter
            else:
                pass # TODO warning
        return filter

    def _set_filter(self, name, suck):
        filter = self._build_filter(name)
        if filter:
            suck[name] = filter

    def _copy_attr_if_set(self, attrs, name, suck):
        if attrs.has_key(name):
            suck[name] = attrs[name]

    def _set_writers(self, suck):
        writers = self._build_writers()
        if writers:
            suck['writers'] = writers

    # Returns [ ( writer, kwargs ), ... ] for all know writer-s.
    def _build_writers(self):
        writers = []
        for writer in ConfigParser.writer_names:
            if self._stack.has_key(writer):
                for kwargs in self._stack[writer]:
                    writers.append((writer, kwargs))
        return writers
